package com.bugsnag.ohos;

import com.bugsnag.ohos.annotation.VisibleForTesting;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Attempts to detect whether the device is rooted. Root detection errs on the side of false
 * negatives rather than false positives.
 * <p>
 * This class will only give a reasonable indication that a device has been rooted - as it's
 * possible to manipulate Java return values & native library loading, it will always be possible
 * for a determined application to defeat these root checks.
 *
 * @since 2021-07-05
 */
public class RootDetector {
    private static final File BUILD_PROP_FILE = new File("/system/build.prop");
    private static final List<String> ROOT_INDICATORS = new ArrayList();
    private AtomicBoolean libraryLoaded = new AtomicBoolean(false);

    static {
        ROOT_INDICATORS.add("/system/xbin/su");
        ROOT_INDICATORS.add("/system/bin/su");
        ROOT_INDICATORS.add("/system/app/Superuser.apk");
        ROOT_INDICATORS.add("/system/app/SuperSU.apk");
        ROOT_INDICATORS.add("/system/app/Superuser");
        ROOT_INDICATORS.add("/system/app/SuperSU");
        ROOT_INDICATORS.add("/system/xbin/daemonsu");
        ROOT_INDICATORS.add("/su/bin");
    }

    private DeviceBuildInfo deviceBuildInfo;
    private List<String> rootBinaryLocations;
    private File buildProps;
    private Logger logger;

    /**
     * constructor
     *
     * @param logger
     */
    public RootDetector(Logger logger) {
        this(DeviceBuildInfo.defaultInfo(), ROOT_INDICATORS, BUILD_PROP_FILE, logger);
    }

    /**
     * constructor
     *
     * @param deviceBuildInfo
     * @param rootBinaryLocations
     * @param buildProps
     * @param logger
     */
    public RootDetector(DeviceBuildInfo deviceBuildInfo, List<String> rootBinaryLocations,
                        File buildProps, Logger logger) {
        this.deviceBuildInfo = deviceBuildInfo;
        this.rootBinaryLocations = rootBinaryLocations;
        this.buildProps = buildProps;
        this.logger = logger;

        try {
            System.loadLibrary("bugsnag-root-detection");
            libraryLoaded.set(true);
        } catch (UnsatisfiedLinkError ignored) {
            ignored.getMessage();
        }
    }

    /**
     * Determines whether the device is rooted or not.
     *
     * @return isRooted
     */
    public boolean isRooted() {
        if (checkBuildTags()) {
            return true;
        }
        if (checkSuExists()) {
            return true;
        }
        if (checkBuildProps()) {
            return true;
        }
        if (checkRootBinaries()) {
            return true;
        }
        if (nativeCheckRoot()) {
            return true;
        }
        return false;
    }

    /**
     * Checks whether the su binary exists by running `which su`. A non-empty result
     * indicates that the binary is present, which is a good indicator that the device
     * may have been rooted.
     *
     * @return boolean
     */
    private boolean checkSuExists() {
        return checkSuExists(new ProcessBuilder());
    }

    @VisibleForTesting
    boolean checkSuExists(ProcessBuilder processBuilder) {
        List<String> list = new ArrayList<>();
        list.add("which");
        list.add("su");
        processBuilder.command(list);
        boolean result;
        Process process = null;
        try {
            process = processBuilder.start();
            InputStream output = process.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(output, Charset.forName("UTF-8"));
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader, 8 * 1024);
            StringWriter buffer = new StringWriter();
            copyTo(bufferedReader, buffer);
            result = !buffer.toString().isEmpty();
        } catch (IOException e) {
            e.getMessage();
            result = false;
        } finally {
            process.destroy();
        }
        return result;
    }

    /**
     * Checks whether the build tags contain 'test-keys', which indicates that the OS was signed
     * with non-standard keys.
     *
     * @return boolean
     */
    public boolean checkBuildTags() {
        String tags = deviceBuildInfo.tags;
        if (tags != null) {
            return tags.contains("test-keys");
        }
        return false;
    }

    /**
     * Checks whether common root binaries exist on disk, which are a good indicator of whether
     * the device is rooted.
     *
     * @return boolean
     */
    public boolean checkRootBinaries() {
        for (String candidate : rootBinaryLocations) {
            if (new File(candidate).exists()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks the contents of /system/build.prop to see whether it contains dangerous properties.
     * These properties give a good indication that a phone might be using a custom
     * ROM and is therefore rooted.
     *
     * @return boolean
     */
    public boolean checkBuildProps() {
        try {
            FileInputStream fileInputStream = new FileInputStream(buildProps);
            InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader, 8 * 1024);
            return bufferedReader.lines()
                .map(line -> line.replace("\\s", ""))
                .filter(line -> line.startsWith("ro.debuggable=[1]") || line.startsWith("ro.secure=[0]"))
                .count() > 0;
        } catch (FileNotFoundException e) {
            e.getMessage();
        } catch (UnsupportedEncodingException e) {
            e.getMessage();
        }
        return false;
    }

    /**
     * copy
     *
     * @param bufferedReader
     * @param writer
     * @return charsCopied
     */
    public long copyTo(BufferedReader bufferedReader, Writer writer) {
        long charsCopied = 0L;
        char[] charsBuffer = new char[8 * 1024];
        int chars = 0;
        try {
            chars = bufferedReader.read(charsBuffer, 0, charsBuffer.length);
            while (chars >= 0) {
                writer.write(charsBuffer, 0, chars);
                charsCopied += chars;
                chars = bufferedReader.read(charsBuffer, 0, charsBuffer.length);
            }
        } catch (IOException e) {
            e.getMessage();
        }
        return charsCopied;
    }

    private native boolean performNativeRootChecks();

    /**
     * Performs root checks which require native code.
     *
     * @return boolean
     */
    private boolean nativeCheckRoot() {
        if (libraryLoaded.get()) {
            return performNativeRootChecks();
        }
        return false;
    }
}
